﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections;
using System.Collections.Concurrent;
using System.Threading;
using System.Runtime.Remoting;
using System.Threading.Tasks;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Diagnostics;
using System.Xml.Linq;

using King.Text;
using King.Reflection;
using King.Extensions;

namespace King.Text.Xml {

    [TextNodeMarkup]
    public abstract class TXNode<T> : TextNode {
    }
    
    public abstract class TXNodes<T> : TXNode<T>, IEnumerable<object> {

        private List<object> m_objects;

        public TXNodes() {
            m_objects = new List<object>();
        }

        protected internal IEnumerable<object> Children() {
            return m_objects;
        }
        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            return Children();
        }

        public void Add(object element) {
            m_objects.Add(element);
        }
        IEnumerator<object> IEnumerable<object>.GetEnumerator() {
            throw new NotSupportedException("XmNodes can only be enumerated during a traversal by TextGenerator.");
        }
        IEnumerator IEnumerable.GetEnumerator() {
            throw new NotSupportedException("XmNodes can only be enumerated during a traversal by TextGenerator.");
        }
    }
    
    [TextNodeContainsContent]
    public sealed class TXFormat<T> : TXNodes<T> {
        public static readonly object OpenElement = new TXFormat<T>("<");
        public static readonly object OpenSlashElement = new TXFormat<T>("</");
        public static readonly object CloseElement = new TXFormat<T>(">");
        public static readonly object CloseSlashElement = new TXFormat<T>("/>");
        public static readonly object Colon = new TXFormat<T>(":");
        public static readonly object Equal = new TXFormat<T>("=");
        public static readonly object OpenComment = new TXFormat<T>("<!--");
        public static readonly object CloseComment = new TXFormat<T>("-->");
        public static readonly object OpenCData = new TXFormat<T>("<![CDATA[");
        public static readonly object CloseCData = new TXFormat<T>("]]>");
        public static readonly object OpenProcessingInstruction = new TXFormat<T>("<?");
        public static readonly object CloseProcessingInstruction = new TXFormat<T>("?>");
        public static readonly object OpenDeclaration = new TXFormat<T>("<?xml");
        public static readonly object CloseDeclaration = new TXFormat<T>("?>");

        private object m_value;

        internal TXFormat(object value) {
            m_value = value;
        }

        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            return new object[] { m_value };
        }
    }

    [TextNodeContainsContent]
    public sealed class TXPrefix<T> : TXNode<T> {

        internal static readonly TXPrefix<T> Xmlns = new TXPrefix<T>(XNamespace.Xmlns, "xmlns");

        private TXNamespace<T> m_ns;
        private string m_value;

        internal TXPrefix(TXNamespace<T> ns, string value) {
            m_ns = ns;
            m_value = value;
        }

        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            return new[] { m_value };
        }

        public string Value {
            get { return m_value; }
        }
        public TXNamespace<T> Namespace {
            get { return m_ns; }
        }
    }

    [TextNodeContainsContent]
    public sealed class TXDeclaration<T> : TXNode<T> {

        public object m_version;
        public object m_encoding;
        public object m_standalone;

        public TXDeclaration(object version = null, object encoding = null, object standalone = null) {
            m_version = version;
            m_encoding = encoding;
            m_standalone = standalone;
        }

        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            yield return TXFormat<T>.OpenDeclaration;
            yield return new TextJoin(prefix: " ") {
                new TextJoin(prefix: "version=\"", suffix: "\"") { m_version },
                new TextJoin(prefix: "encoding=\"", suffix: "\"") { m_encoding },
                new TextJoin(prefix: "standalone=\"", suffix: "\"") { m_standalone },
            };
            yield return TXFormat<T>.CloseDeclaration;
        }
    }

    [TextNodeContainsContent]
    public sealed class TXLocalName<T> : TXNode<T> {

        private XName m_name;

        internal TXLocalName(XName name) {
            m_name = name;
        }

        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            return new object[] { m_name.LocalName };
        }

        public TXName<T> Name {
            get { return m_name; }
        }
        public TXNamespace<T> Namespace {
            get { return m_name.Namespace; }
        }
        public string NamespaceName {
            get { return m_name.NamespaceName; }
        }
        public string LocalName {
            get { return m_name.LocalName; }
        }

        public override string ToString() {
            return m_name.LocalName;
        }
    }

    [TextNodeContainsContent]
    public sealed class TXNamespace<T> : TXNode<T> {

        public static bool operator !=(TXNamespace<T> left, TXNamespace<T> right) {
            return !(left == right);
        }
        public static bool operator ==(TXNamespace<T> left, TXNamespace<T> right) {
            if ((object)left == (object)right)
                return true;

            if ((object)left == null || (object)right == null)
                return false;

            return left.m_ns == right.m_ns;
        }
        public static TXName<T> operator +(TXNamespace<T> ns, string localName) {
            return ns.m_ns + localName;
        }
        public static implicit operator TXNamespace<T>(XNamespace namespaceName) {
            return GetXmNamespace(namespaceName);
        }
        public static implicit operator TXNamespace<T>(Uri uri) {
            return uri.AbsoluteUri;
        }
        public static implicit operator TXNamespace<T>(string namespaceName) {
            return (XNamespace)namespaceName;
        }

        public static TXNamespace<T> Get(string namespaceName) {
            return (XNamespace)namespaceName;
        }

        public static TXNamespace<T> None {
            get { return XNamespace.None; }
        }
        public static TXNamespace<T> Xml {
            get { return XNamespace.Xml; }
        }
        public static TXNamespace<T> Xmlns {
            get { return XNamespace.Xmlns; }
        }

        private static TXNamespace<T> GetXmNamespace(XNamespace ns) {
            TXNamespace<T> cached;
            if (!s_xNamespaceToXmNamespace.TryGetValue(ns, out cached))
                s_xNamespaceToXmNamespace[ns] = cached = new TXNamespace<T>(ns);
            return cached;
        }

        static TXNamespace() {
            s_xNamespaceToXmNamespace = new Dictionary<XNamespace, TXNamespace<T>>();
        }

        private static Dictionary<XNamespace, TXNamespace<T>> s_xNamespaceToXmNamespace;

        private XNamespace m_ns;

        private TXNamespace(XNamespace ns) {
            m_ns = ns;
        }

        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            return new[] { m_ns.NamespaceName };
        }

        public string NamespaceName {
            get { return m_ns.NamespaceName; }
        }
        public TXName<T> GetName(string localName) {
            return m_ns.GetName(localName);
        }

        public override bool Equals(object obj) {
            return this == obj;
        }
        public override int GetHashCode() {
            return base.GetHashCode();
        }
        public override string ToString() {
            return m_ns.ToString();
        }
    }

    [TextNodeContainsContent]
    public sealed class TXName<T> : TXNode<T> {

        public static bool operator !=(TXName<T> left, TXName<T> right) {
            return !(left == right);
        }
        public static bool operator ==(TXName<T> left, TXName<T> right) {
            if ((object)left == (object)right)
                return true;

            if ((object)left == null || (object)right == null)
                return false;

            return left.m_name == right.m_name;
        }
        public static implicit operator TXName<T>(XName name) {
            return GetXmName(name);
        }
        public static implicit operator TXName<T>(string expandedName) {
            return (XName)expandedName;
        }

        public static TXName<T> Get(string expandedName) {
            return GetXmName(XName.Get(expandedName));
        }
        public static TXName<T> Get(string localName, string namespaceName) {
            return GetXmName(XName.Get(localName, namespaceName));
        }

        internal static TXName<T> GetXmName(XName name) {
            TXName<T> cached;
            if (!s_xNameToXmName.TryGetValue(name, out cached))
                s_xNameToXmName[name] = cached = new TXName<T>(name);
            return cached;
        }

        static TXName() {
            s_xNameToXmName = new Dictionary<XName, TXName<T>>();
        }

        private static Dictionary<XName, TXName<T>> s_xNameToXmName;

        private XName m_name;

        private TXName(XName name) {
            m_name = name;
        }

        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            TXPrefix<T> prefix = null;

            if (Namespace == TXNamespace<T>.Xmlns) {
                prefix = TXPrefix<T>.Xmlns;

            } else if (Namespace != TXNamespace<T>.None) {
                prefix = context.Ancestors<TXElement<T>>()
                    .Select(o => o.GetPrefixOrDefault(Namespace))
                    .FirstOrDefault(o => o != null);

                if (prefix == null)
                    throw new ArgumentException("Could not find a prefix for namespace: " + Namespace);

                if (prefix.Value == "xmlns")
                    prefix = null;
            }

            if (prefix != null) {
                yield return prefix;
                yield return TXFormat<T>.Colon;
            }

            yield return new TXLocalName<T>(m_name.LocalName);
        }

        public string LocalName {
            get { return m_name.LocalName; }
        }
        public TXNamespace<T> Namespace {
            get { return m_name.Namespace; }
        }
        public string NamespaceName {
            get { return m_name.NamespaceName; }
        }

        public override bool Equals(object obj) {
            return this == obj;
        }
        public override int GetHashCode() {
            return base.GetHashCode();
        }
        public override string ToString() {
            return m_name.ToString();
        }
    }
    public sealed class TXAttribute<T> : TXNode<T> {

        private TXName<T> m_name;
        private bool m_useSingleQuotes;
        private bool m_force;
        private object m_value;

        public TXAttribute(TXName<T> name, object value = null, bool useSingleQuotes = false, bool force = false) {
            Contract.Requires(name != null);
            Contract.Requires(name.Namespace != TXNamespace<T>.Xmlns ||
                value is XNamespace || value is TXNamespace<T> || value is string || value is Uri);

            m_value = value;
            m_name = name;
            m_useSingleQuotes = useSingleQuotes;
            m_force = force;
        }

        protected internal override IEnumerable<object> Content(TextNodeContext context) {
            if (m_force)
                return ArbitraryContent;

            return new[] { m_value };
        }
        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            var hasContent = context.ContainsContent(m_value);
            if (!hasContent && !m_force)
                yield break;

            var delimiter = m_useSingleQuotes ? "'" : "\"";

            yield return m_name;
            yield return TXFormat<T>.Equal;
            yield return delimiter;
            yield return new TXMarkupText<T>() { m_value };
            yield return delimiter;
        }

        public object Value {
            get { return m_value; }
        }
        public TXNamespace<T> Namespace {
            get { return m_name.Namespace; }
        }
        public TXName<T> Name {
            get { return m_name; }
        }
        public string LocalName {
            get { return Name.LocalName; }
        }
    }

    [TextNodeContainsContent]
    public sealed class TXProcessingInstruction<T> : TXNodes<T> {
        private object m_name;

        public TXProcessingInstruction(object name) {
            m_name = name;
        }

        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            yield return TXFormat<T>.OpenProcessingInstruction;
            yield return new TextJoin() { m_name, Children() };
            yield return TXFormat<T>.CloseProcessingInstruction;
        }
    }

    public sealed class TXElement<T> : TXNodes<T> {

        private TXName<T> m_name;
        private object m_attributes;
        private bool m_inline;
        private Dictionary<TXNamespace<T>, TXPrefix<T>> m_prefixMap;

        public TXElement(TXName<T> name, object attributes = null, bool inline = false) {
            Contract.Requires(attributes.ToEnumerable().All(o => typeof(TXAttribute<T>).CanAssignValue(o)));

            m_name = name;
            m_attributes = attributes;
            m_inline = inline;
        }

        private TXNamespace<T> GetNamespace(object value) {
            if (value is string)
                return (TXNamespace<T>)(string)value;

            if (value is Uri)
                return (TXNamespace<T>)(Uri)value;

            if (value is XNamespace)
                return (XNamespace)value;

            return (TXNamespace<T>)value;
        }
        private void InitializePrefixMap(TextNodeContext context) {
            var attributes = context.Flatten(m_attributes);

            var xmlns =
                from attribute in attributes.Cast<TXAttribute<T>>()
                where attribute.Namespace == TXNamespace<T>.Xmlns ||
                     (attribute.Namespace == XNamespace.None && attribute.Name.LocalName == "xmlns")
                let ns = GetNamespace(attribute.Value)
                select new {
                    Namespace = ns,
                    Prefix = new TXPrefix<T>(ns, attribute.LocalName),
                };

            if (xmlns.Any())
                m_prefixMap = xmlns.ToDictionary(o => o.Namespace, o => o.Prefix);
        }

        internal TXPrefix<T> GetPrefixOrDefault(TXNamespace<T> ns) {

            if (m_prefixMap == null)
                return null;

            TXPrefix<T> result;
            if (!m_prefixMap.TryGetValue(ns, out result))
                return null;

            return result;
        }

        protected internal override IEnumerable<object> MarkupContent() {
            return Children();
        }
        protected internal override IEnumerable<object> Content(TextNodeContext context) {
            return ArbitraryContent;
        }
        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            InitializePrefixMap(context);

            var hasChildren = context.ContainsContent(Children());

            yield return TXFormat<T>.OpenElement;
            yield return new TextJoin() { 
                m_name, 
                m_attributes 
            };

            if (!hasChildren) {
                yield return " ";
                yield return TXFormat<T>.CloseSlashElement;

            } else {
                yield return TXFormat<T>.CloseElement;

                object children = Children();

                if (!m_inline) {
                    children = new TextIndent() {
                        new TextSingleSpaced(
                            prefix: Environment.NewLine, 
                            suffix: Environment.NewLine) 
                        { 
                            children 
                        }
                    };
                }

                yield return new TXText<T>() { children };
                
                yield return TXFormat<T>.OpenSlashElement;
                yield return m_name;
                yield return TXFormat<T>.CloseElement;
            }
        }

        public TXNamespace<T> Namespace {
            get { return m_name.Namespace; }
        }
        public TXName<T> Name {
            get { return m_name; }
        }
        public string LocalName {
            get { return Name.LocalName; }
        }
    }

    [TextNodeEscapeTargets(new[] { '<', '>', '&', '\'', '"' })]
    public abstract class TXEscape<T> : TXNodes<T> {

        protected internal override IEnumerable<object> MarkupContent() {
            return Children();
        }
        protected internal override string Escape(TextNodeContext context, char source) {

            switch (source) {
                case '>': return "&gt;";
                case '<': return "&lt;";
                case '&': return "&amp;";
                case '\'': return "&apos;";
                case '"': return "&quot;";
                default: return null;
            }
        }
    }

    [TextNodeEndMarkup]
    public sealed class TXText<T> : TXEscape<T> {
    }

    public sealed class TXMarkupText<T> : TXEscape<T> {
    }

    public sealed class TXComment<T> : TXNodes<T> {

        public TXComment() {
        }

        protected internal override IEnumerable<object> Content(TextNodeContext context) {
            return Children();
        }
        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            var hasContent = context.ContainsContent(Children());
            if (!hasContent)
                yield break;

            var content = new TXCommentContent<T>() {
                new TXMarkupText<T>() {
                    new TextSingleSpaced() { 
                        Children() 
                    }
                }
            };

            yield return TXFormat<T>.OpenComment;
            if (!context.ContainsNewLine(Children())) {
                yield return content;

            } else {
                yield return Environment.NewLine;
                yield return content;
                yield return Environment.NewLine;
            }
            yield return TXFormat<T>.CloseComment;
        }
    }
    public sealed class TXCommentContent<T> : TXNodes<T> {
        internal TXCommentContent() { }
    }
    public sealed class TXCData<T> : TXNodes<T> {

        protected internal override IEnumerable<object> MarkupContent() {
            return Children();
        }
        protected internal override IEnumerable<object> Content(TextNodeContext context) {
            return Children();
        }
        protected internal override IEnumerable<object> Children(TextNodeContext context) {
            
            if (context.Ancestors<TXCData<T>>().Any())
                throw new ArgumentException("CData sections cannot be nested.");

            if (!context.ContainsContent(Children()))
                yield break;

            yield return TXFormat<T>.OpenCData;
            yield return Children();
            yield return TXFormat<T>.CloseCData;
        }
    }

    [TextNodeSubstituteTargets(
        typeof(XNode),
        typeof(XName),
        typeof(XNamespace),
        typeof(XAttribute),
        typeof(XDeclaration)
    )]
    public sealed class TXLinq<T> : TXNodes<T> {

        private class Context {
            private Dictionary<string, XNamespace> m_prefixToNamespace;
            private HashSet<XNamespace> m_namespaces;

            internal Context(Context parent) {
                m_prefixToNamespace = new Dictionary<string, XNamespace>(parent.m_prefixToNamespace);
            }
            internal Context() {
                m_prefixToNamespace = new Dictionary<string, XNamespace>();
            }

            private bool NamespaceHasPrefix(XNamespace ns) {
                Contract.Assert(ns != null);

                if (ns == XNamespace.Xmlns)
                    return true;

                if (ns == XNamespace.None)
                    return true;

                if (m_namespaces == null)
                    m_namespaces = new HashSet<XNamespace>(m_prefixToNamespace.Values);

                return m_namespaces.Contains(ns);
            }
            private Context NewContext(IEnumerable<XAttribute> prefixes, XNamespace defaultNamespace) {
                var result = new Context(this);

                if (defaultNamespace != null)
                    result.m_prefixToNamespace[string.Empty] = defaultNamespace;

                foreach (var prefix in prefixes)
                    result.m_prefixToNamespace[prefix.Name.LocalName] = (XNamespace)(string)prefix;

                return result;
            }

            internal Context GetContext(XElement xml, out TXAttribute<T> xmlsnAttribute) {
                xmlsnAttribute = null;
                var result = this;

                // group attributes by namespace (excluding default the namespace attribute)
                var attributesByNamespace =
                   (from o in xml.Attributes()
                    where o.Name.LocalName != "xmlns"
                    group o by o.Name.Namespace)
                    .ToDictionary(o => o.Key, o => (IEnumerable<XAttribute>)o);

                // project all namespace prefix attributes
                IEnumerable<XAttribute> prefixes;
                if (!attributesByNamespace.TryGetValue(XNamespace.Xmlns, out prefixes))
                    prefixes = Enumerable.Empty<XAttribute>();

                // build a new context for this element if there are any new prefixes defined
                var defaultNamespace = xml.GetDefaultNamespace();
                if (attributesByNamespace.Any() || defaultNamespace != XNamespace.None)
                    result = NewContext(prefixes, defaultNamespace);

                // find attribute namespaces without prefixes
                var namespacesWithoutPrefixes = attributesByNamespace.Keys.Where(o => !result.NamespaceHasPrefix(o)).ToList();

                // test if the element itself has a namespace without a prefix
                if (!result.NamespaceHasPrefix(xml.Name.Namespace))
                    namespacesWithoutPrefixes.Add(xml.Name.Namespace);

                var count = namespacesWithoutPrefixes.Count;
                if (count > 0) {

                    // if there is more than one namespace without a prefix and or a defaultNamespace is already defined then throw
                    if ((count == 1 && defaultNamespace != XNamespace.None) || count > 1)
                        throw new ArgumentException(
                            "The following namespaces have no prefix and the default namespace has already been specified " +
                            "or there is more than one namespace without a prefix: " +
                            new TextJoin(", ") { namespacesWithoutPrefixes.Select(o => o.NamespaceName) }.ReadToEnd());

                    // a single namespace has no prefix and there is no existing defaultNamespace
                    defaultNamespace = namespacesWithoutPrefixes.Single();
                    result = NewContext(prefixes, defaultNamespace);
                    xmlsnAttribute = new TXAttribute<T>("xmlns", defaultNamespace);
                }

                return result;
            }
        }

        private IEnumerable<object> SubstituteElement(Context context, XElement element) {

            var attributes = element.Attributes().Select(o => SubstituteAttribute(o));
            var children = element.Nodes().Select(o => Substitute(context, o));

            // add a default namespace if needed and possible given the current context
            TXAttribute<T> xmlnsAttribute;
            context = context.GetContext(element, out xmlnsAttribute);
            if (xmlnsAttribute != null)
                attributes = new[] { xmlnsAttribute }.Concat(attributes);

            return new TXElement<T>(element.Name, attributes) { children };
        }
        private IEnumerable<object> SubstituteDocument(Context context, XDocument document) {
            yield return document.Declaration;
            yield return Environment.NewLine;
            yield return document.Nodes().Select(o => Substitute(context, o));
        }
        private TXDeclaration<T> SubstituteDeclaration(XDeclaration declaration) {
            return new TXDeclaration<T>(declaration.Version, declaration.Encoding, declaration.Standalone);
        }
        private TXProcessingInstruction<T> SubstituteProcessingInstruction(XProcessingInstruction processingInstruction) {
            return new TXProcessingInstruction<T>(processingInstruction.Target) { processingInstruction.Data };
        }
        private TXAttribute<T> SubstituteAttribute(XAttribute attribute) {
            return new TXAttribute<T>(attribute.Name, attribute.Value);
        }
        private TXComment<T> SubstituteComment(XComment comment) {
            return new TXComment<T>() { comment.Value };
        }
        private TXText<T> SubstituteText(XText text) {
            return new TXText<T>() { text.Value };
        }
        private TXName<T> SubstituteName(XName name) {
            return (TXName<T>)name;
        }
        private TXNamespace<T> SubstituteNamespace(XNamespace ns) {
            return (TXNamespace<T>)ns;
        }
        private TXCData<T> SubstituteCData(XCData cdata) {
            return new TXCData<T>() { cdata.Value };
        }
        private object Substitute(Context context, object value) {
            if (value is XElement)
                return SubstituteElement(context, (XElement)value);

            else if (value is XAttribute)
                return SubstituteAttribute((XAttribute)value);

            else if (value is XComment)
                return SubstituteComment((XComment)value);

            else if (value is XCData)
                return SubstituteCData((XCData)value);

            else if (value is XText)
                return SubstituteText((XText)value);

            else if (value is XName)
                return SubstituteName((XName)value);

            else if (value is XNamespace)
                return SubstituteNamespace((XNamespace)value);

            else if (value is XProcessingInstruction)
                return SubstituteProcessingInstruction((XProcessingInstruction)value);

            else if (value is XDeclaration)
                return SubstituteDeclaration((XDeclaration)value);

            else if (value is XDocument)
                return SubstituteDocument(context, (XDocument)value);

            return null;
        }

        protected internal override IEnumerable<object> Substitute(TextNodeContext context, object value) {
            var result = Substitute(new Context(), value);
            if (result == null)
                return null;

            return new[] { result };
        }
    }
}
